<!DOCTYPE html>
<html lang="zh-CN">





<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/img/apple-touch-icon.png">
  <link rel="icon" type="image/png" href="/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
  <meta name="description" content="安全行业从业者，主要方向PC逆向附带安卓和Linux逆向，时不时喜欢写点BUG代码">
  <meta name="author" content="Cray">
  <meta name="keywords" content="">
  <title>编写基础调试器：第三部分（调用堆栈/寄存器/上下文） ~ 逆向安全博客</title>

  <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/5.12.1/css/all.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/css/bootstrap.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/mdbootstrap/4.13.0/css/mdb.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/3.0.1/github-markdown.min.css"  >

<link rel="stylesheet" href="//at.alicdn.com/t/font_1067060_qzomjdt8bmp.css">



  <link rel="stylesheet" href="/lib/prettify/tomorrow.min.css"  >

<link rel="stylesheet" href="/css/main.css"  >


  <link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css"  >


<meta name="generator" content="Hexo 5.2.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">


    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/">首页</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/archives/">归档</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/tags/">标签</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/links/">友链</a>
          </li>
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="view intro-2" id="background" false
      style="background: url('https://gitee.com/cve/BlogImg/raw/master/VM_tools/image-20201123095837741.png') no-repeat center center;
      background-size: cover;">
    
        <div class="full-bg-img">
        <div class="mask rgba-black-light flex-center">
          <div class="container text-center white-text fadeInUp">
            <span class="h2" id="subtitle">
              
                编写基础调试器：第三部分（调用堆栈/寄存器/上下文）
              
            </span>

            
              <br>
              

              <p>
                
                  
                  &nbsp;<i class="far fa-chart-bar"></i>
                  <span class="post-count">
                    2.8k 字
                  </span>&nbsp;
                

                
                  
                  &nbsp;<i class="far fa-clock"></i>
                  <span class="post-count">
                      12 分钟
                  </span>&nbsp;
                

                
                  <!-- 不蒜子统计文章PV -->
                  
                  &nbsp;<i class="far fa-eye" aria-hidden="true"></i>&nbsp;
                  <span id="busuanzi_container_page_pv">
                    <span id="busuanzi_value_page_pv"></span> 次
                  </span>&nbsp;
                
              </p>
            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="py-5 z-depth-3" id="board">
        <div class="post-content mx-auto" id="post">
          <div class="markdown-body">
            <p>到目前为止，编写调试器时讨论的所有功能都与将调试器附加到进程以及能够设置断点和执行单步操作有关。尽管此功能肯定有用，但它是被动调试的功能：您可以在特定点中断过程的状态，并在指令级对其进行检测，但是您实际上无法修改任何行为，甚至无法查看过程如何达到该状态。接下来将要介绍的核心功能将详细说明实际上能够查看和更改程序执行状态（以线程上下文的形式，即寄存器），以及能够在遇到断点时查看线程的调用堆栈。</p>
<h4 id="线程上下文"><a href="#线程上下文" class="headerlink" title="线程上下文"></a>线程上下文</h4><p><a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681917(v=vs.85).aspx">与Windows相关的定义的</a>线程上下文“在线程进程的地址空间中包括线程的计算机寄存器集，内核堆栈，线程环境块和用户堆栈”。对于这些模式中正在开发的用户模式调试器，重要的部分是机器寄存器和用户堆栈。该[线程环境块](<a target="_blank" rel="noopener" href="http://undocumented.ntinternals.net/source/usermode/undocumented">http://undocumented.ntinternals.net/source/usermode/undocumented</a> functions/nt objects/thread/teb.html)也是从用户模式访问，但不会在这里介绍，由于其无证和非常具体的性质。当进程启动时，加载程序将设置进程主线程，并在入口点开始执行。该主线程又可以启动其他线程，这些线程本身也可以启动线程，依此类推。这些线程中的每一个都有自己的上下文，其中包含上面列出的项目。</p>
<p>这些上下文的目的是，作为抢占式多任务操作系统，Windows可以在任何时间点中断任何[usermode]任务，例如执行代码的线程。在这些中断期间，将执行上下文切换，这只是保存当前执行上下文并设置要执行的新上下文的过程。最终，当原任务计划恢复时，上下文切换将再次发生回到原始线程的上下文，并且它将继续执行，就好像什么都没发生一样。这些上下文是什么样的？答案是它完全是处理器专用的，考虑到它们存储寄存器，这也就不足为奇了。</p>
<p>在Windows中，开发人员可用的线程上下文部分在<em>winnt.h中</em>定义为<em>CONTEXT</em>结构。例如，以下是x86处理器的<em>CONTEXT</em>结构的片段。</p>
<pre><code class="C">typedef struct _CONTEXT &#123;
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    ...
    FLOATING_SAVE_AREA FloatSave;
    DWORD   SegGs;
    DWORD   SegFs;
    ...
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    ...
    DWORD   Ebp;
    DWORD   Eip;
    ...</code></pre>
<p>x64版本的结构起来很接近，寄存器宽度扩展到了64位，并增加了其他寄存器和扩展名</p>
<pre><code class="C">typedef struct DECLSPEC_ALIGN(16) _CONTEXT &#123;
    DWORD ContextFlags;
    DWORD MxCsr;
    ...
    WORD   SegGs;
    WORD   SegSs;
    DWORD EFlags;
    ...
    DWORD64 Rax;
    DWORD64 Rcx;
    DWORD64 Rdx;
    ...
    DWORD64 R13;
    DWORD64 R14;
    DWORD64 R15;
    ...</code></pre>
<p>这是调试时最有用的检查和修改的结构。 调试器应该能够打印出此结构，并允许对其任何字段进行修改。幸运的是，有两个非常有用的API用于检索和修改此结构：<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms679362(v=vs.85).aspx">GetThreadContext</a>和<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680632(v=vs.85).aspx">SetThreadContext</a>。先前在讨论如何启用单步操作时已经介绍了这些内容。恢复上下文，修改<em>EFlags</em>。那么，为了添加此功能，需要对现有代码/逻辑进行哪些修改？这是很简单的通过打开当前正在执行的线程的句柄（或在调试中命中断点）并检查/设置上下文。</p>
<pre><code class="c++">const CONTEXT Debugger::GetExecutingContext()
&#123;
    CONTEXT ctx = &#123; 0 &#125;;
    ctx.ContextFlags = CONTEXT_ALL;
    SafeHandle hThread = OpenCurrentThread();
    if (hThread.IsValid())
    &#123;
        bool bSuccess = BOOLIFY(GetThreadContext(hThread(), &amp;ctx));
        if (!bSuccess)
        &#123;
            fprintf(stderr, &quot;Could not get context for thread %X. Error = %X\n&quot;, m_dwExecutingThreadId, GetLastError());
        &#125;
    &#125;

    memcpy(&amp;m_lastContext, &amp;ctx, sizeof(CONTEXT));

    return ctx;
&#125;

const bool Debugger::SetExecutingContext(const CONTEXT &amp;ctx)
&#123;
    bool bSuccess = false;
    SafeHandle hThread = OpenCurrentThread();
    if (hThread.IsValid())
    &#123;
        bSuccess = BOOLIFY(SetThreadContext(hThread(), &amp;ctx));
    &#125;

    memcpy(&amp;m_lastContext, &amp;ctx, sizeof(CONTEXT));

    return bSuccess;
&#125;</code></pre>
<p>对于每次访问或修改，都会去打开（或关闭）当前线程的句柄-这显然不是个高效的方法，但足以满足演示目的。然后，上下文状态存储在<em>m_lastContext中</em>。当流程收到<em>EXCEPTION_BREAKPOINT时，</em>以及单步执行流程（即处理<em>EXCEPTION_SINGLE_STEP</em>异常）时，将调用这些函数。因此，<em>m_lastContext</em>当遇到断点或用户单步执行时，在上下文结构中将始终保存着实时的寄存器值。当用户想要通过调试器接口修改一个或多个特定寄存器时，也可以调用这些功能。打印上下文只涉及结构中的值。我示例代码中选择只打印最常用的几个寄存器：</p>
<pre><code class="C++">void Debugger::PrintContext()
&#123;
#ifdef _M_IX86
    fprintf(stderr, &quot;EAX: %p EBX: %p ECX: %p EDX: %p\n&quot;
        &quot;ESP: %p EBP: %p ESI: %p EDI: %p\n&quot;
        &quot;EIP: %p FLAGS: %X\n&quot;,
        m_lastContext.Eax, m_lastContext.Ebx, m_lastContext.Ecx, m_lastContext.Edx,
        m_lastContext.Esp, m_lastContext.Ebp, m_lastContext.Esi, m_lastContext.Edi,
        m_lastContext.Eip, m_lastContext.EFlags);
#elif defined _M_AMD64
    fprintf(stderr, &quot;RAX: %p RBX: %p RCX: %p RDX: %p\n&quot;
        &quot;RSP: %p RBP: %p RSI: %p RDI: %p\n&quot;
        &quot;R8: %p R9: %p R10: %p R11: %p\n&quot;
        &quot;R12: %p R13: %p R14: %p R15: %p\n&quot;
        &quot;RIP: %p FLAGS: %X\n&quot;,
        m_lastContext.Rax, m_lastContext.Rbx, m_lastContext.Rcx, m_lastContext.Rdx,
        m_lastContext.Rsp, m_lastContext.Rbp, m_lastContext.Rsi, m_lastContext.Rdi,
        m_lastContext.R8, m_lastContext.R9, m_lastContext.R10, m_lastContext.R11,
        m_lastContext.R12, m_lastContext.R13, m_lastContext.R14, m_lastContext.R15,
        m_lastContext.Rip, m_lastContext.EFlags);
#else
#error &quot;Unsupported architecture&quot;
#endif
&#125;</code></pre>
<h4 id="调用栈"><a href="#调用栈" class="headerlink" title="调用栈"></a>调用栈</h4><p>在底层，一个函数的功能通过堆栈来决定。这是一个编译器和/或ABI定义的构造，用于安排功能状态的布局。一个程序堆栈通常包括调用方的返回地址，从调用方传递给函数的参数，以及存在于函数范围内的局部变量的空间。排除x86和x64，在其他指令集体系结构中，这些堆栈帧之前带有<em>序言</em>，该*<strong>prologue**<em>是负责设置从调用者到被调用者的堆栈和帧指针（</em>ESP* / <em>EBP</em>或<em>RSP / RBP</em>）的代码。在被调用者返回之前，有一个*</strong>epilogue***，负责将堆栈指针返回给调用方。例如，构造一个这样的C函数：</p>
<pre><code class="C">void TestFunction(int a, int b, int c)
&#123;
    int d = 4, e = 5, f = 6;
&#125;
TestFunction(1,2,3)</code></pre>
<p>在汇编中会通过以下顺序进行调用</p>
<pre><code class="asm">push        3  
push        2  
push        1  
call        TestFunction</code></pre>
<p><code>TestFunction</code>函数使用x86进行反汇编如下</p>
<pre><code class="asm">#-----prologue start-----
push        ebp
mov         ebp,esp  
sub         esp,0Ch  
#-----prologue end  -----
mov         dword ptr [ebp-4],4  
mov         dword ptr [ebp-8],5  
mov         dword ptr [ebp-0Ch],6  
#-----epilogue start-----
mov         esp,ebp  
pop         ebp  
ret         0Ch  
#-----epilogue end  -----</code></pre>
<p><strong>prologue</strong>和<strong>epilogue</strong>用注释显示。在执行<strong>prologue</strong>之后，此函数的堆栈帧将在*[EBP]<em>中包含调用者栈帧指针，</em>[EBP + 4]<em>保存返回地址（因为</em>CALL<em>指令隐式将下一条指令的地址压入堆栈更改执行），以及在</em>[EBP + 8]<em>，</em>[EBP + 12]<em>和</em>[EBP + 16]<em>处传递的参数。<strong>prologue</strong>从栈的底部减去12，以便为局部变量（在函数中声明的三个32位整数）留出空间。如在反汇编中所见，它们将位于</em>[EBP-4]<em>，</em>[EBP-8]<em>和</em>[EBP-12]处*。</p>
<p>这种设定十分便利，因为它可以轻松区分什么时传入参数和局部变量。由于所有内容都保存在堆栈中并通过栈帧指针进行索引，而不是分散在寄存器和堆栈之间，因此调试变得容易一些。当您从x86转到x64时，这会有所变化，其中x64将前四个（或六个，取决于您的编译器/平台）参数存储在寄存器中，其余参数存储在堆栈中。这也可以根据调用约定和编译器优化（尤其是栈帧指针省略）而有所变化。</p>
<blockquote>
<p>x64：函数参数</p>
<p>amd64规范，rdi,rsi,rdx,rcx,r8,r9,栈</p>
<p>ms规范，rcx,rdx,r8,r9,栈</p>
</blockquote>
<p>由于栈帧存储了调用者的返回地址，因此可以看到从哪里调用了函数。所以调用栈：调用栈帧中返回地址所在函数的集合，这些栈帧表示导致当前栈帧的代码中的调用链。就调试而言，此信息很有用，因为在一个函数中出现的错误可能在之前的函数中显现出来。能够快速遍历调用栈并查看他们传入的值。</p>
<p>在Windows平台上，有一个方便的功能：向后执行步行堆栈帧的乏味/烦恼：<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680650(v=vs.85).aspx">StackWalk64</a>。此功能与x86和x64兼容，但是在调用之前确实需要进行一些设置。给定非常特定于机器的堆栈帧布局，<em>StackWalk64</em>函数需要填写<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680646(v=vs.85).aspx">STACKFRAME64</a>结构，并将其作为参数传递给它。填写此结构仅涉及设置指令，帧和堆栈指针以及地址模式，这对于x86和x64上的现代Windows而言将是平面寻址。一旦建立了此结构，就可以在循环中调用<em>StackWalk64</em>来检索帧。放入代码，如下所示：</p>
<pre><code class="C++">void Debugger::PrintCallStack()
&#123;
    STACKFRAME64 stackFrame = &#123; 0 &#125;;
    const DWORD_PTR dwMaxFrames = 50;
    CONTEXT ctx = GetExecutingContext();

    stackFrame.AddrPC.Mode = AddrModeFlat;
    stackFrame.AddrFrame.Mode = AddrModeFlat;
    stackFrame.AddrStack.Mode = AddrModeFlat;

#ifdef _M_IX86
    DWORD dwMachineType = IMAGE_FILE_MACHINE_I386;
    stackFrame.AddrPC.Offset = ctx.Eip;
    stackFrame.AddrFrame.Offset = ctx.Ebp;
    stackFrame.AddrStack.Offset = ctx.Esp;
#elif defined _M_AMD64
    DWORD dwMachineType = IMAGE_FILE_MACHINE_AMD64;
    stackFrame.AddrPC.Offset = ctx.Rip;
    stackFrame.AddrFrame.Offset = ctx.Rbp;
    stackFrame.AddrStack.Offset = ctx.Rsp;
#else
#error &quot;Unsupported platform&quot;
#endif

    SafeHandle hThread = OpenCurrentThread();
    for (int i = 0; i &lt; dwMaxFrames; ++i)
    &#123;
        const bool bSuccess = BOOLIFY(StackWalk64(dwMachineType, m_hProcess(), hThread(), &amp;stackFrame,
            (dwMachineType == IMAGE_FILE_MACHINE_I386 ? nullptr : &amp;ctx), nullptr,
            SymFunctionTableAccess64, SymGetModuleBase64, nullptr));
        if (!bSuccess || stackFrame.AddrPC.Offset == 0)
        &#123;
            fprintf(stderr, &quot;StackWalk64 finished.\n&quot;);
            break;
        &#125;

        fprintf(stderr, &quot;Frame: %X\n&quot;
            &quot;Execution address: %p\n&quot;
            &quot;Stack address: %p\n&quot;
            &quot;Frame address: %p\n&quot;,
            i, stackFrame.AddrPC.Offset,
            stackFrame.AddrStack.Offset, stackFrame.AddrFrame.Offset);
    &#125;
&#125;</code></pre>
<h4 id="测试功能"><a href="#测试功能" class="headerlink" title="测试功能"></a>测试功能</h4><p>为了测试此功能，我们创建另一个演示应用程序，它将用作调试目标。 代码如下：</p>
<pre><code class="C">#include &lt;cstdio&gt;

void d()
&#123;
    printf(&quot;d called.\n&quot;);
&#125;

void c()
&#123;
    printf(&quot;c called.\n&quot;);
    d();
&#125;

void b()
&#123;
    printf(&quot;b called.\n&quot;);
    c();
&#125;

void a()
&#123;
    printf(&quot;a called.\n&quot;);
    b();
&#125;

int main(int argc, char *argv[])
&#123;
    printf(&quot;Addresses: \n&quot;
        &quot;a: %p\n&quot;
        &quot;b: %p\n&quot;
        &quot;c: %p\n&quot;
        &quot;d: %p\n&quot;,
        a, b, c, d);

    getchar();
    while (true)
    &#123;
        a();
        getchar();
    &#125;

    return 0;
&#125;</code></pre>
<p>为了方便起见，建议在编译选项中<strong>禁用代码优化</strong>和<strong>ASLR</strong>。 下面是在d函数中设置断点并命中时<strong>Visual Studio</strong>生成的堆栈跟踪。</p>
<pre><code class="json">Demo.exe!d() Line 5    C++
Demo.exe!c() Line 14    C++
Demo.exe!b() Line 20    C++
Demo.exe!a() Line 26    C++
Demo.exe!main(int argc, char * * argv) Line 41    C++
Demo.exe!__tmainCRTStartup() Line 626    C
Demo.exe!mainCRTStartup() Line 466    C
kernel32.dll!@BaseThreadInitThunk@12()    Unknown
ntdll.dll!___RtlUserThreadStart@8()    Unknown
ntdll.dll!__RtlUserThreadStart@8()    Unknown</code></pre>
<p>附加调试器也会产生10帧，如下所示：</p>
<pre><code class="json">a
Target address: 0x4010f0
Received breakpoint at address 004010F0
Press c to continue or s to begin stepping.
l
Frame: 0
Execution address: 004010F0
Stack address: 00000000
Frame address: 0018FBE4
Frame: 1
Execution address: 004010DA
Stack address: 00000000
Frame address: 0018FBE8
Frame: 2
Execution address: 0040108A
Stack address: 00000000
Frame address: 0018FCBC
Frame: 3
Execution address: 0040103A
Stack address: 00000000
Frame address: 0018FD90
Frame: 4
Execution address: 004011C6
Stack address: 00000000
Frame address: 0018FE64
Frame: 5
Execution address: 00401699
Stack address: 00000000
Frame address: 0018FF38
Frame: 6
Execution address: 004017DD
Stack address: 00000000
Frame address: 0018FF88
Frame: 7
Execution address: 75D5338A
Stack address: 00000000
Frame address: 0018FF90
Frame: 8
Execution address: 77339F72
Stack address: 00000000
Frame address: 0018FF9C
Frame: 9
Execution address: 77339F45
Stack address: 00000000
Frame address: 0018FFDC
StackWalk64 finished.</code></pre>
<p>输出的效果不及Visual Studio调试器，但结果是正确的。 最后将名称和地址放在一起。 就是符号加载和映射进入的地方，这将是下一篇文章的主题。</p>
<p>文章来源：<a target="_blank" rel="noopener" href="http://www.codereversing.com/blog/archives/170">http://www.codereversing.com/blog/archives/170</a></p>

            <hr>
          </div>
          <br>
          <div>
            <p>
            
            
              <span>
                <i class="iconfont icon-tag"></i>
                
                  <a class="hover-with-bg" href="/tags/%E9%80%86%E5%90%91/">逆向</a>
                
              </span>
            
            </p>
            
              <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a target="_blank" href="https://zh.wikipedia.org/wiki/Wikipedia:CC_BY-SA_3.0%E5%8D%8F%E8%AE%AE%E6%96%87%E6%9C%AC" rel="nofollow noopener noopener">CC BY-SA 3.0协议</a> 。转载请注明出处！</p>
            
          </div>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container">
        <div id="toc">
  <p class="h5"><i class="far fa-list-alt"></i>&nbsp;目录</p>
  <div id="tocbot"></div>
</div>
      </div>
    
  </div>
</div>

<!-- custom -->


<!-- Comments -->
<div class="col-lg-7 mx-auto nopadding-md">
  <div class="container comments mx-auto" id="comments">
    
      <br><br>
      
      
  <div class="disqus" style="width:100%">
    <div id="disqus_thread"></div>
    <script>
      var disqus_config = function () {
        this.page.url = 'http://cve.gitee.io/cve/2020/11/22/编写基础调试器：第三部分（调用堆栈_寄存器_上下文）/';
        this.page.identifier = '/2020/11/22/编写基础调试器：第三部分（调用堆栈_寄存器_上下文）/';
      };
      var oldLoad = window.onload;
      window.onload = function () {
        var d = document, s = d.createElement('script');
        s.type = 'text/javascript';
        s.src = '//' + 'crayon-1' + '.disqus.com/embed.js';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
      };
    </script>
    <noscript>Please enable JavaScript to view the <a target="_blank" href="https://disqus.com/?ref_noscript" rel="nofollow noopener noopener">comments
        powered by Disqus.</a></noscript>
  </div>


    
  </div>
</div>

    
  </main>

  
    <a class="z-depth-1" id="scroll-top-button" href="#" role="button">
      <i class="fa fa-chevron-up scroll-top-arrow" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  <footer class="mt-5">
  <div class="text-center py-3">
    <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><b>Hexo</b></a>
    <i class="iconfont icon-love"></i>
    <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"> <b>Fluid</b></a>
    <br>

    
  
    <!-- 不蒜子统计PV -->
    
    &nbsp;<span id="busuanzi_container_site_pv"></span>总访问量 
          <span id="busuanzi_value_site_pv"></span> 次&nbsp;
  
  
    <!-- 不蒜子统计UV -->
    
    &nbsp;<span id="busuanzi_container_site_uv"></span>总访客数 
            <span id="busuanzi_value_site_uv"></span> 人&nbsp;
  
  <br>



    


    <!-- cnzz Analytics icon -->
    

  </div>
</footer>

<!-- SCRIPTS -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script src="https://cdn.staticfile.org/popper.js/1.16.1/umd/popper.min.js" ></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/js/bootstrap.min.js" ></script>
<script src="https://cdn.staticfile.org/mdbootstrap/4.13.0/js/mdb.min.js" ></script>
<script src="/js/main.js" ></script>


  <script src="/js/lazyload.js" ></script>



  
  <script src="https://cdn.staticfile.org/tocbot/4.10.0/tocbot.min.js" ></script>
  <script>
    $(document).ready(function () {
      var navHeight = $('#navbar').height();
      var toc = $('#toc');
      var main = $('main');
      var tocT = navHeight + (toc.offset().top - main.offset().top);
      var tocLimMin = main.offset().top - navHeight;
      var tocLimMax = $('#comments').offset().top - navHeight;
      $(window).scroll(function () {
        var scroH = document.body.scrollTop + document.documentElement.scrollTop;
        if (tocLimMin <= scroH && scroH <= tocLimMax) {
          toc.css({
            'display': 'block',
            'position': 'fixed',
            'top': tocT,
          });
        } else if (scroH <= tocLimMin) {
          toc.css({
            'position': '',
            'top': '',
          });
        } else if (scroH > tocLimMax) {
          toc.css('display', 'none');
        }
      });
      tocbot.init({
        tocSelector: '#tocbot',
        contentSelector: '.post-content',
        headingSelector: 'h1,h2,h3,h4,h5,h6',
        linkClass: 'tocbot-link',
        activeLinkClass: 'tocbot-active-link',
        listClass: 'tocbot-list',
        isCollapsedClass: 'tocbot-is-collapsed',
        collapsibleClass: 'tocbot-is-collapsible',
        scrollSmooth: true,
      });
      if ($('.toc-list-item').length > 0) {
        $('#toc > p').css('visibility', 'visible');
      }
    });
  </script>







  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>


<!-- Plugins -->



  <script src="https://cdn.staticfile.org/prettify/188.0.0/prettify.min.js" ></script>
  <script>
    $(document).ready(function () {
      $('pre').addClass('prettyprint  linenums');
      prettyPrint();
    })
  </script>





  <script src="https://cdn.staticfile.org/anchor-js/4.2.2/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "right",
      visible: "hover",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script src="/js/local-search.js" ></script>
  <script>
    var path = "/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      getSearchFile(path);
      this.onclick = null
    }
  </script>



  <script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <script>
    $("#post img:not(.no-zoom img, img[no-zoom])").each(
      function () {
        var element = document.createElement("a");
        $(element).attr("data-fancybox", "images");
        $(element).attr("href", $(this).attr("src"));
        $(this).wrap(element);
      }
    );
  </script>












</body>
</html>
